import pako from 'pako';
import { caeErrorLog, caeWarnLog } from '../common/utils';
import { SocketEventHandler, SocketStatusListener } from '../types/socket';
import WsTunnel from './WsTunnel';

export class SocketEngine {
  private static instance: SocketEngine;
  private tunnel: WsTunnel = null;
  private connectionPromise: Promise<SocketEngine> = null;
  private documentId: string = '';
  private cmdIndex: number = 0;
  private cmdMap: Map<number, any> = new Map();
  private eventHandlers = new Set<SocketEventHandler>();
  /**
   * 状态变化监听器，发生在打开和关闭时
   */
  private statusListeners = new Set<SocketStatusListener>();
  /**
   * 自定义二进制数据解码函数
   * @param buffer
   * @returns
   */
  private decodeBinaryDataFunc: (buffer: Uint8Array) => void;

  private healthCheckCommand = 'HealthCheck';
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private heartbeatInterval = 30000;
  private heartbeatCounter = 0;

  private constructor() {}

  public static getInstance(): SocketEngine {
    if (!SocketEngine.instance) {
      SocketEngine.instance = new SocketEngine();
    }
    return SocketEngine.instance;
  }

  /**
   * 尝试建立WebSocket连接，如果已经连接，直接返回；
   * 连接中则返回Promise，调用者等待连接成功；
   * 未连接则进行连接并返回Promise；
   * @param documentId 发送到后端的数据都会带一个“s”字段，就是文档id
   * @param wssUrl WebSocket连接url
   */
  static async connect(
    documentId: string,
    wssUrl: string | URL,
  ): Promise<SocketEngine> {
    const engine = SocketEngine.getInstance();
    if (engine.isOpen) {
      return engine;
    }
    if (engine.tunnel?.isConnecting) {
      return engine.connectionPromise;
    }
    engine.connectionPromise = engine.open(documentId, wssUrl);
    return engine.connectionPromise;
  }

  private async open(documentId: string, wssUrl: string | URL) {
    try {
      const engine = SocketEngine.getInstance();
      engine.tunnel = new WsTunnel(wssUrl);
      await engine.tunnel.open();
      engine.startHeartbeat();

      engine.documentId = documentId;
      engine.cmdIndex = 0;
      engine.cmdMap = new Map();
      engine.tunnel.attach('data', (evt: any) => {
        engine.onMessage(evt);
      });
      engine.tunnel.attach('status', (opened: any) => {
        if (!opened) {
          engine.stop();
        }
      });
      return engine;
    } catch (e) {
      // caeErrorLog('failed to start engine', e);
      alert('failed to start engine');
      throw e;
    }
  }

  get isOpen() {
    return this.tunnel?.isOpen;
  }

  async stop() {
    this.stopHeartbeat();

    try {
      this.tunnel.close();
      this.tunnel = null;
      this.documentId = '';
      this.cmdIndex = 0;
      this.statusListeners.forEach((listener) => listener(false));

      // 发送给所有CMD响应，清空命令队列
      this.cmdMap.forEach((solver) => {
        solver.reject({ code: -1 });
      });
      this.cmdMap.clear();
    } catch (error) {
      caeErrorLog('failed to stop core engine', error);
      throw new Error('Failed to stop core engine');
    }
  }

  registerEventHandler(eventHandler: SocketEventHandler) {
    this.eventHandlers.add(eventHandler);
  }

  unregisterEventHandler(eventHandler: SocketEventHandler) {
    this.eventHandlers.delete(eventHandler);
  }

  addStatusListener(listener: SocketStatusListener) {
    this.statusListeners.add(listener);
  }

  removeStatusListener(listener: SocketStatusListener) {
    this.statusListeners.delete(listener);
  }

  /**
   * 设置自定义的二进制解码函数，代替默认的gzip压缩的json解压方法
   * @param func
   */
  setDecodeBinaryDataFunc(func: (buffer: Uint8Array) => void) {
    this.decodeBinaryDataFunc = func;
  }

  execCmd(cmd: any, returnFullResponse = false): Promise<any> {
    if (cmd.c !== this.healthCheckCommand) {
      this.resetHeartbeat();
    }

    cmd.s = this.documentId;
    cmd.i = ++this.cmdIndex;
    cmd.z = true;
    const result = new Promise((resolve, reject) => {
      this.cmdMap.set(this.cmdIndex, { resolve, reject, returnFullResponse });
      this.tunnel.send(JSON.stringify(cmd));
    });
    return result;
  }

  async onMessage(raw: any) {
    let data = raw;
    let result: any;
    try {
      if (typeof raw === 'string') {
        result = JSON.parse(data);
      } else {
        // 如果有自定义的二进制解码函数则调用
        if (this.decodeBinaryDataFunc) {
          if (data instanceof Blob) {
            const ab = await data.arrayBuffer();
            const uint8Array = new Uint8Array(ab);
            result = this.decodeBinaryDataFunc(uint8Array);
          } else if (data instanceof ArrayBuffer) {
            const uint8Array = new Uint8Array(data);
            result = this.decodeBinaryDataFunc(uint8Array);
          } else {
            console.warn('Unknown data type:', typeof data);
          }
        } else {
          // 默认二进制是gzip压缩的json，这里进行解压缩和解析
          data = await this.readCompressedBlob(raw);
          result = JSON.parse(data);
        }
      }
    } catch (error) {
      caeErrorLog('GspEngine onMessage', {
        data,
        error,
      });

      return;
    }
    if (!result) {
      caeErrorLog('result is null');
      return;
    }
    if (result.event) {
      // 接收websocket所推数据
      this.eventHandlers.forEach((eventHandler) => {
        eventHandler(result);
      });
      return;
    }
    const solver = this.cmdMap.get(result.index);
    if (solver) {
      this.cmdMap.delete(result.index);
      if (result.code === 0) {
        solver.resolve(solver.returnFullResponse ? result : result.data);
      } else {
        solver.reject({ code: result.code, message: result.message });
      }
    } else {
      caeWarnLog('未知请求结果', result);
      if (result && result.code != 0) {
        // 其他未知错误处理，发送给所有CMD响应，比如401错误（token失效）
        this.cmdMap.forEach((solver) => {
          solver.reject({ code: result.code, message: result.message });
        });
        // 全部发送后清空Map
        this.cmdMap.clear();
      }
    }
  }

  async readCompressedBlob(blob: Blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = function () {
        const data = pako.inflate(reader.result as any);
        resolve(new TextDecoder().decode(data));
      };
      reader.onerror = function (err) {
        reject(err);
      };
      reader.readAsArrayBuffer(blob);
    });
  }

  createWsUrlWithCurLoc(uri: string): string | URL {
    if (process.env.NODE_ENV === 'development') {
      return `wss://${process.env.REACT_APP_NEXT_URL}${uri}`;
      // return `wss://next-dev.amcax.net${uri}`;
    }
    const url = new URL(uri, window.location.href);
    url.protocol = url.protocol.replace('http', 'ws');
    return url;
  }

  private startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.heartbeatCounter++;
      if (this.heartbeatCounter > 50) {
        this.stopHeartbeat();
      } else {
        this.execCmd({ c: this.healthCheckCommand });
      }
    }, this.heartbeatInterval);
  }

  private stopHeartbeat() {
    this.heartbeatCounter = 0;
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  private resetHeartbeat() {
    this.stopHeartbeat();
    this.startHeartbeat();
  }
}
